package gobelinmaker.server;

import com.esotericsoftware.kryonet.Connection;
import java.awt.Dimension;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.util.LinkedList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import javax.media.jai.Interpolation;
import javax.media.jai.InterpolationNearest;
import javax.media.jai.JAI;
import javax.media.jai.PerspectiveTransform;
import javax.media.jai.PlanarImage;
import org.imgscalr.Scalr;

/**
 * Szerver kamera objektum oszálya.
 *
 * @author imruf84
 */
public abstract class ServerCamera extends LinkedList<Connection> {

    /**
     * Kamera azonosítója.
     */
    private final String ID;
    /**
     * Eszköz sorszáma.
     */
    private final int deviceID;
    /**
     * Kamera neve.
     */
    private final String name;
    /**
     * Felbontás.
     */
    private final Dimension resolution;
    /**
     * Képeket készítő szál.
     */
    private Thread thread = null;
    /**
     * Tényleges kamera.
     */
    private final AtomicReference<GobelinCamera> gobelinCamera = new AtomicReference<>(null);
    /**
     * Szerver.
     */
    private final GobelinServer gs;
    /**
     * Tű kalibrációs helye.
     */
    private final Point2D needleCalibrationPoint = new Point2D.Double(0.5, 0.5);
    /**
     * Kamera kalibrációs pontjai.
     */
    private final Point2D cameraCalibrationPoints[] = {new Point2D.Double(0, 0), new Point2D.Double(1, 0), new Point2D.Double(1, 1), new Point2D.Double(0, 1)};
    /**
     * Egyéb mátrix.
     */
    private AffineTransform transform = null;
    /**
     * Fényerő.
     */
    private float brightness = 0;
    /**
     * Kontraszt.
     */
    private float contrast = 100;
    /**
     * Fókusz.
     */
    private float focus = -1;

    /**
     * JAI figyelmeztetés letiltása miatt kell.
     */
    static {
        System.setProperty("com.sun.media.jai.disableMediaLib", "true");
    }

    /**
     * Konstruktor.
     *
     * @param ID kamera azonosítója
     * @param name kamera neve
     * @param gs szerver
     * @param tr egyéb transzformáció
     * @param deviceID eszköz sorszáma
     * @param resoultion felbontás
     * @param focus fókusz
     */
    public ServerCamera(String ID, String name, GobelinServer gs, AffineTransform tr, int deviceID, Dimension resoultion, float focus) {
        this.name = name;
        this.ID = ID;
        this.gs = gs;
        this.transform = tr;
        this.deviceID = deviceID;
        this.resolution = resoultion;
        this.focus = focus;
    }

    /**
     * Képek készítésének elindítása.
     *
     * @param virtual igaz esetén virtuális kamerák lesznek
     * @param frequency képkészítés gyakorisága
     */
    public void start(final boolean virtual, final int frequency) {

        final ServerCamera sc = this;

        // Fut már szál, akkor előbb leállítjuk.
        if (null != this.thread && this.thread.isAlive()) {
            return;
        }

        final AtomicBoolean isEnd = new AtomicBoolean(false);

        // Alkalmazás leállásának a figyelése.
        Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("stop camera:" + getCameraID());
                isEnd.set(true);
            }
        }));

        // Szál futtatása.
        this.thread = new Thread(new Runnable() {
            @Override
            public void run() {

                GobelinCamera camera;
                try {
                    camera = (virtual ? new GobelinImageCamera(gs) : new GobelinRealCamera(gs, getDeviceID(), sc));

                    gobelinCamera.set(camera);
                    while (!isEnd.get()) {

                        // Kép elkészítése.
                        BufferedImage image = camera.getImage();

                        // Ha nincs kép akkor kilépünk.
                        if (null == image) {
                            break;
                        }

                        // Új kép elkészültének a jelzése.
                        newImageCreated(image, new Runnable() {

                            @Override
                            public void run() {
                                try {
                                    Thread.sleep(frequency);
                                } catch (InterruptedException ex) {
                                    System.err.println(ex.getLocalizedMessage());
                                }
                            }
                        });
                    }

                    // Kamera leállítása (ha lehetséges).
                    camera.close();

                } catch (Exception ex) {
                    System.err.println(ex.getLocalizedMessage());
                }
                // Szál futásának a befejezése.
                thread.interrupt();
            }
        });

        this.thread.setDaemon(true);
        this.thread.start();
    }

    /**
     * Kamera azonosítójának a lekérdezése.
     *
     * @return kamera azonosítója
     */
    public String getCameraID() {
        return this.ID;
    }

    /**
     * Eszköz sorszámának a lekérdezése.
     *
     * @return eszköz sorszáma
     */
    public int getDeviceID() {
        return deviceID;
    }

    /**
     * Felbontás lekérdezése.
     *
     * @return felbontás
     */
    public Dimension getResolution() {
        return resolution;
    }

    /**
     * Kamera nevének a lekérdezése.
     *
     * @return kamera neve
     */
    public String getCameraName() {
        return this.name;
    }

    /**
     * Kamera lekérdezése.
     *
     * @return kamera
     */
    public GobelinCamera getCamera() {
        return this.gobelinCamera.get();
    }

    /**
     * Új kép elkészültének az eseménye.
     *
     * @param image elkészült kép
     */
    public abstract void newImageCreated(final BufferedImage image);

    /**
     * Új kép elkészültének az eseménye.
     *
     * @param image elkészült kép
     * @param complete sikeres művelet eseménye
     */
    private void newImageCreated(BufferedImage image, Runnable complete) {

        BufferedImage lImage = image;

        // Kép transzformálása, ha szükséges.
        if (null != this.transform) {
            Interpolation interp = new InterpolationNearest();
            PlanarImage result = (PlanarImage) JAI.create("affine", lImage, this.transform, interp);
            lImage = result.getAsBufferedImage();
        }

        // Események futtatása.
        newImageCreated(lImage);
        if (null != complete) {
            complete.run();
        }
    }

    /**
     * Tű kalibrációs pontjának a megadása.
     *
     * @param p kalibrációs pont
     */
    public void setNeedleCalibrationPoint(Point2D p) {
        this.needleCalibrationPoint.setLocation(p.getX(), p.getY());
    }

    /**
     * Tű kalibrációs pontjának a lekérdezése.
     *
     * @return tű kalibrációs pontja
     */
    public Point2D getNeedleCalibrationPoint() {
        return this.needleCalibrationPoint;
    }

    /**
     * Tű kalibrációs adata válaszként.
     *
     * @return tű kalibrációs adata
     */
    public NeedleCalibrationPointResponse needleCalibrationDataToResponse() {

        NeedleCalibrationPointResponse result = new NeedleCalibrationPointResponse();
        result.p = new Point2D.Double(getNeedleCalibrationPoint().getX(), getNeedleCalibrationPoint().getY());

        return result;
    }

    /**
     * Kamera kalibrációs pontjainak a megadása.
     *
     * @param pTL bal felső pont
     * @param pTR jobb felső pont
     * @param pBR jobb alsó pont
     * @param pBL bal alsó pont
     */
    public void setCameraCalibrationPoints(Point2D pTL, Point2D pTR, Point2D pBR, Point2D pBL) {
        this.cameraCalibrationPoints[0].setLocation(pTL.getX(), pTL.getY());
        this.cameraCalibrationPoints[1].setLocation(pTR.getX(), pTR.getY());
        this.cameraCalibrationPoints[2].setLocation(pBR.getX(), pBR.getY());
        this.cameraCalibrationPoints[3].setLocation(pBL.getX(), pBL.getY());
    }

    /**
     * Kamera kalibrációs pontjainak a lekérdezése.
     *
     * @return kamera kalibrációs pontjai
     */
    public Point2D[] getCameraCalibrationPoints() {
        return this.cameraCalibrationPoints;
    }

    /**
     * Kamera kalibrációs adata válaszként.
     *
     * @return kamera kalibrációs adata
     */
    public CameraCalibrationPointsResponse cameraCalibrationDataToResponse() {
        CameraCalibrationPointsResponse result = new CameraCalibrationPointsResponse();

        result.pTL = new Point2D.Double(this.cameraCalibrationPoints[0].getX(), this.cameraCalibrationPoints[0].getY());
        result.pTR = new Point2D.Double(this.cameraCalibrationPoints[1].getX(), this.cameraCalibrationPoints[1].getY());
        result.pBR = new Point2D.Double(this.cameraCalibrationPoints[2].getX(), this.cameraCalibrationPoints[2].getY());
        result.pBL = new Point2D.Double(this.cameraCalibrationPoints[3].getX(), this.cameraCalibrationPoints[3].getY());

        return result;
    }

    /**
     * Kamera beállítások adata válaszként.
     *
     * @return kamera beállítások adata
     */
    public CameraColorAdjustmentResponse cameraColorAdjustmentDataToResponse() {

        CameraColorAdjustmentResponse ccar = new CameraColorAdjustmentResponse();
        ccar.brightness = this.getBrightness();
        ccar.contrast = this.getContrast();
        ccar.focus = this.getFocus();

        return ccar;
    }

    /**
     * Kép transzformálása.
     *
     * @param image transzformálandó kép
     * @param points transzformáció pontjai
     * @return transzformált kép
     * @throws java.awt.geom.NoninvertibleTransformException kivétel
     * @throws java.lang.CloneNotSupportedException kivétel
     */
    public static BufferedImage transformImage(final BufferedImage image, Point2D points[]) throws NoninvertibleTransformException, CloneNotSupportedException {

        // Eredeti képmérettel dolgozunk eredetileg.
        int w = image.getWidth();
        int h = image.getHeight();

        // Új sarkok kiszámítása.
        Point2D tl = new Point2D.Double(points[0].getX() * w, points[0].getY() * h);
        Point2D tr = new Point2D.Double(points[1].getX() * w, points[1].getY() * h);
        Point2D bl = new Point2D.Double(points[3].getX() * w, points[3].getY() * h);
        Point2D br = new Point2D.Double(points[2].getX() * w, points[2].getY() * h);

        // Végeredmény.
        BufferedImage result = new BufferedImage(w, h, image.getType());

        // Transzformáció meghatározása.
        PerspectiveTransform p = PerspectiveTransform.getQuadToQuad(
                tl.getX(), tl.getY(), bl.getX(), bl.getY(), br.getX(), br.getY(), tr.getX(), tr.getY(),
                0, 0, 0, h, w, h, w, 0).createInverse();

        // Új kép létrehozása.
        for (int x = 0; x < w; x++) {
            for (int y = 0; y < h; y++) {
                Point2D P = p.transform(new Point2D.Double(x, y), null);
                int rgb = image.getRGB(Math.min(w - 1, Math.max(0, Math.round((float) P.getX()))), Math.min(h - 1, Math.max(0, Math.round((float) P.getY()))));
                result.setRGB(x, y, rgb);
            }
        }

        // Új kép méretének minimalizálása.
        w = (int) Math.round(Math.max(Math.max(tl.getX(), tr.getX()), Math.max(bl.getX(), br.getX())) - Math.min(Math.min(tl.getX(), tr.getX()), Math.min(bl.getX(), br.getX())));
        h = (int) Math.round(Math.max(Math.max(tl.getY(), tr.getY()), Math.max(bl.getY(), br.getY())) - Math.min(Math.min(tl.getY(), tr.getY()), Math.min(bl.getY(), br.getY())));

        // Új kép átméretezése.
        result = Scalr.resize(
                result, Scalr.Method.ULTRA_QUALITY,
                ((double) w / (double) w) < ((double) result.getWidth() / (double) result.getHeight()) ? Scalr.Mode.FIT_TO_WIDTH : Scalr.Mode.FIT_TO_HEIGHT,
                w, h, Scalr.OP_ANTIALIAS);

        return result;
    }

    /**
     * Fényerő megadása.
     *
     * @param brightness fényerő
     */
    public void setBrightness(float brightness) {
        this.brightness = brightness;
    }

    /**
     * Fényerő lekérdezése.
     *
     * @return fényerő
     */
    public float getBrightness() {
        return this.brightness;
    }

    /**
     * Kontraszt megadása.
     *
     * @param contrast Kontraszt
     */
    public void setContrast(float contrast) {
        this.contrast = contrast;
    }

    /**
     * Kontraszt lekérdezése.
     *
     * @return Kontraszt
     */
    public float getContrast() {
        return this.contrast;
    }

    /**
     * Fókusz megadása.
     *
     * @param focus fókusz
     */
    public void setFocus(float focus) {
        this.focus = focus;
    }

    /**
     * Fókusz lekérdezése.
     *
     * @return fókusz
     */
    public float getFocus() {
        return this.focus;
    }
}
